package com.wlf.server.common.service;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.wlf.server.common.context.BeanContext;
import com.wlf.server.common.em.CacheKey;
import com.wlf.server.common.em.Const;
import com.wlf.server.common.entity.RoleMenu;
import com.wlf.server.common.entity.RoleUser;
import com.wlf.server.common.entity.SysMenu;
import com.wlf.server.common.mapper.SysMenuMapper;
import com.wlf.server.web.dto.MappingDTO;
import com.wlf.server.web.dto.MenuDTO;
import com.wlf.server.web.util.AjaxBean;
import com.wlf.server.web.util.AjaxError;
import com.wlf.server.web.util.JSON;
import com.wlf.server.web.util.ServletUtil;
import lombok.Getter;
import lombok.Setter;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.Serial;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

@Service
@CacheConfig(cacheNames = CacheKey.menu)
public class SysMenuService extends ServiceImpl<SysMenuMapper, SysMenu> {
    @Resource
    private MappingDTO mappingDTO;

    /**
     * 更新或添加菜单
     * 更新保存的根据就是ID是否为空。
     */
    @Caching(
            evict = {
                    @CacheEvict(key = "#menuDTO.id", condition = "#menuDTO.id != null "),
                    @CacheEvict(cacheNames = CacheKey.power, allEntries = true, condition = "#menuDTO.type == 2"),
                    @CacheEvict(cacheNames = CacheKey.user, allEntries = true)
            }
    )
    @Transactional
    public AjaxBean saveOrUpdate(MenuDTO menuDTO) {
        if (!menuDTO.getPid().equals("0")) {
            SysMenu pidMenu = BeanContext.getBean(this.getClass()).getMenu(menuDTO.getPid());
            menuDTO.setMenuLevel(pidMenu.getMenuLevel() + 1);
            if (menuDTO.getMenuLevel() > 3) {
                if (Objects.equals(menuDTO.getType(), Const.MenuType.MENU.getId())) {
                    return AjaxBean.getError("最多只能设置四级菜单");
                }
            }
            // 如果上级为按钮则不允许再次增加子内容
            if (pidMenu.getType().equals(Const.MenuType.BUTTON.getId())) {
                // 合法操作的情况下是进不来的。
                return AjaxBean.getError("上级为菜单类别为按钮时不允许添加子内容");
            }
        }
        if (StrUtil.isBlank(menuDTO.getId())) {
            menuDTO.setId(IdWorker.getIdStr());
            // 判断父级菜单下是否没有子菜单
            Long count = this.lambdaQuery().eq(SysMenu::getPid, menuDTO.getPid()).count();

            if (count == 0) {
                // 如果没有则遍历拥有父级菜单的角色，新增的这条记录也添加到此角色信息中。
                List<RoleMenu> list = Db.lambdaQuery(RoleMenu.class).eq(RoleMenu::getMenuId, menuDTO.getPid()).list();
                for (RoleMenu roleMenu : list) {
                    RoleMenu rm = new RoleMenu();
                    rm.setRoleId(roleMenu.getRoleId());
                    rm.setMenuId(menuDTO.getId());
                    Db.save(rm);
                }
                // 最后删除父级菜单对应的角色信息
                Db.lambdaUpdate(RoleMenu.class).eq(RoleMenu::getMenuId, menuDTO.getPid()).remove();
            }
            return AjaxBean.getByBool(super.save(mappingDTO.to(menuDTO)));
        } else {
            return AjaxBean.getByBool(super.updateById(mappingDTO.to(menuDTO)));
        }

    }


    public AjaxBean get(String id) {
        return AjaxBean.getOkData(BeanContext.getBean(this.getClass()).getMenu(id));
    }

    @Cacheable(key = "#id")
    public SysMenu getMenu(String id) {
        return this.getById(id);
    }

    /**
     * 删除
     * 删除父级时会连带删除他下面的所有子集
     */
    @Caching(
            evict = {
                    @CacheEvict(allEntries = true),
                    @CacheEvict(cacheNames = CacheKey.power, allEntries = true),
                    @CacheEvict(cacheNames = CacheKey.user, allEntries = true)

            }
    )
    public AjaxBean remove(String id) {
        List<String> ids = new ArrayList<>();
        depMenuIds(id, ids);
        return AjaxBean.getByBool(Db.removeByIds(ids,this.entityClass));
    }

    /**
     * 递归找出所有子集菜单ID（包括自己）
     */
    private void depMenuIds(String id, List<String> ids) {
        ids.add(id);
        LambdaQueryWrapper<SysMenu> eq = new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getPid, id);
        List<SysMenu> list = this.list(eq);
        if (!list.isEmpty()) {
            list.forEach(i -> depMenuIds(i.getId(), ids));
        }
    }

    /**
     * 转树结构
     *
     * @param sysMenus 原始一维数据
     * @param tConfig  树的配置递归深度
     */
    public List<Tree<String>> getTree(List<SysMenu> sysMenus, TreeNodeConfig tConfig) {
        //转换器
        return TreeUtil.build(sysMenus, "0", tConfig,
                (menu, tree) -> {
                    tree.setId(menu.getId());
                    tree.setParentId(menu.getPid());
                    tree.setWeight(menu.getSort());
                    tree.setName(menu.getName());
                    tree.putExtra("icon", menu.getIcon());
                    tree.putExtra("type", menu.getType());
                    tree.putExtra("info", menu.getInfo());
                    tree.putExtra("url", menu.getUrl());
                    tree.putExtra("isShow", menu.getIsShow());
                    tree.putExtra("menuLevel", menu.getMenuLevel());
                    tree.putExtra("isBlank", menu.getIsBlank());
                    tree.putExtra("permission", menu.getPermission());
                });
    }

    /**
     * 要作为缓存的，必须要实现Serializable这个接口
     */
    @Getter
    @Setter
    public static class UrlAndPower implements Serializable {
        @Serial
        private static final long serialVersionUID = 1L;
        // 对应的Url
        private String url;
        // 权限标识符
        private String power;
    }


    /**
     * 用了缓存，拦截器里的拦截链，
     * 使用缓存，按钮的增删改，会全部删除此缓存
     */
    @Cacheable(cacheNames = CacheKey.power)
    public List<UrlAndPower> urlAndPowerList() {
        return this.lambdaQuery().eq(SysMenu::getType, Const.MenuType.BUTTON.getId())
                .list().stream().map(i -> {
                    UrlAndPower power = new UrlAndPower();
                    power.setUrl(i.getUrl());
                    power.setPower(i.getPermission());
                    return power;
                }).toList();
    }

    //

    /**
     * 获取当前登录用户的拥有的菜单。
     * 由于保存时不保存父节点，为啥不保存父节点，这里面涉及到前端一个bug。需要慢慢领悟。
     * 又不是不能用
     * 这里查出的是所有子节点，需要递归查出父节点。
     * 查出所有的菜单
     */
    public List<SysMenu> getSysMenuByLoginUser() {
        ArrayList<SysMenu> allMenu = new ArrayList<>();

        //获取登录用户的角色Id
        List<String> roleIds = Db.lambdaQuery(RoleUser.class)
                .eq(RoleUser::getUserId, StpUtil.getLoginIdAsString()).list()
                .stream().map(RoleUser::getRoleId).toList();
        // 获取菜单信息
        List<String> menuIds = Db.lambdaQuery(RoleMenu.class)
                .in(RoleMenu::getRoleId, roleIds).list()
                .stream().map(RoleMenu::getMenuId).toList();

        List<SysMenu> list = this.lambdaQuery().in(SysMenu::getId, menuIds)
                .list();
        for (SysMenu menu : list) {
            getAllMenus(menu, allMenu);
        }
        // 去重并去除按钮返回
        return allMenu.stream().
                collect(Collectors.collectingAndThen(
                        Collectors.toCollection(() -> new TreeSet<>
                                (Comparator.comparing(SysMenu::getId))),
                        ArrayList::new))
                .stream().filter(i -> Objects.equals(i.getType(), Const.MenuType.MENU.getId()))
                .collect(Collectors.toList());
    }

    /**
     * 递归向上查找父级菜单
     */
    private void getAllMenus(SysMenu sysMenu, ArrayList<SysMenu> list) {
        list.add(sysMenu);
        if (!sysMenu.getPid().equals("0")) {
            getAllMenus(BeanContext.getBean(this.getClass()).getById(sysMenu.getPid()), list);
        }
    }

    public AjaxBean parentList() {
        // 查出所有的菜单
        List<SysMenu> list = this.lambdaQuery().ne(SysMenu::getType, Const.MenuType.BUTTON.getId()).list();
        SysMenu menu = new SysMenu();
        menu.setPid("0");
        menu.setId("0");
        menu.setName("根菜单");
        list.add(menu);
        TreeNodeConfig tConfig = new TreeNodeConfig();
        // 自定义属性名 都要默认值的
        tConfig.setWeightKey("sort");
        tConfig.setIdKey("value");
        tConfig.setChildrenKey("children");
        tConfig.setNameKey("label");
        // 最大递归深度
        tConfig.setDeep(4);
        //转换器
        List<Tree<String>> treeNodes = TreeUtil.build(list, "0", tConfig,
                (treeNode, tree) -> {
                    tree.setId(treeNode.getId());
                    tree.setParentId(treeNode.getPid());
                    tree.setWeight(treeNode.getSort());
                    tree.setName(treeNode.getName());
                });
        return AjaxBean.getOkData(treeNodes);
    }

    /**
     * 导出菜单JSON文件
     */
    public void exportMenu() {
        String fileName = FileUtil.getTmpDirPath() + IdWorker.getIdStr() + ".json";
        FileUtil.writeLines(this
                .list().stream().map(
                        JSON::toJSONString).toList(), new File(fileName), StandardCharsets.UTF_8);
        try {
            HttpServletResponse response = ServletUtil.getResponse();
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                    ContentDisposition.attachment().filename(
                                    StrUtil.format("菜单文件{}.json", DateUtil.date()
                                            .toString(DatePattern.UTC_SIMPLE_MS_PATTERN)), StandardCharsets.UTF_8)
                            .build().toString()
            );
            FileCopyUtils.copy(new FileInputStream(fileName), response.getOutputStream());
        } catch (Exception e) {
            log.error("导出失败", e);
        }
    }

    /**
     * 根据上传的JSON文件重新生成菜单
     */
    @Transactional
    @Caching(
            evict = {
                    @CacheEvict(allEntries = true),
                    @CacheEvict(cacheNames = CacheKey.power, allEntries = true),
                    @CacheEvict(cacheNames = CacheKey.user, allEntries = true)

            }
    )
    public AjaxBean importMenu(MultipartFile file) {
        File tempFile = FileUtil.createTempFile();
        try {
            file.transferTo(tempFile);
        } catch (Exception e) {
            return AjaxBean.getError("导入文件失败");
        }
        // 先删除再保存
        if (!this.lambdaUpdate().remove() || !this.saveBatch(FileUtil.readUtf8Lines(tempFile).stream()
                .map(i -> JSON.parseObject(i, SysMenu.class)).toList())) {
            AjaxError.getAndThrow("导入菜单数据失败");
        }
        return AjaxBean.getOkData("导入菜单数据成功");
    }

}
